package com.lee.auth.endpoint;

import com.fasterxml.jackson.core.type.TypeReference;
import com.lee.auth.dto.request.*;
import com.lee.auth.dto.response.AccessToken;
import com.lee.auth.dto.response.TokenUser;
import com.lee.auth.endpoint.ClientHolder.ClientInfo;
import com.lee.auth.endpoint.verifier.OneKeyVerifier;
import com.lee.auth.endpoint.verifier.ThirdVerifier;
import com.lee.auth.endpoint.verifier.ThirdVerifierHolder;
import com.lee.auth.entity.BaseUser;
import com.lee.auth.entity.BaseUserThird;
import com.lee.auth.mapper.BaseUserMapper;
import com.lee.auth.mapper.BaseUserThirdMapper;
import com.lee.auth.security.authority.Authority;
import com.lee.auth.security.context.AuthContext;
import com.lee.auth.security.context.AuthContextHolder;
import com.lee.auth.security.token.TokenPayloadOuter.TokenPayload;
import com.lee.auth.security.token.TokenService;
import com.lee.auth.sms.SmsScene;
import com.lee.auth.sms.SmsService;
import com.lee.common.exception.CustomException;
import com.lee.common.json.SafeJsonMapper;
import com.lee.common.transaction.TransactionOps;
import com.lee.common.vo.ResultVO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import static com.lee.common.exception.CustomException.BAD_PARAM_EXC;
import static com.lee.common.exception.CustomException.NONENTITY_USER_EXC;
import static com.lee.common.exception.FailureStatus.*;
import static java.lang.System.currentTimeMillis;

/**
 * @author Lee HN
 * @date 2021/4/12 9:43
 */
@Slf4j
@RestController
@RequestMapping("/login")
@AllArgsConstructor
public class LoginEndpoint {

    private final PasswordEncoder passwordEncoder;

    private final ClientHolder clientHolder;

    private final TokenService tokenService;

    private final ThirdVerifierHolder thirdVerifierHolder;

    private final SmsService smsService;

    private final SafeJsonMapper jsonMapper;

    private final BaseUserMapper baseUserMapper;

    private final BaseUserThirdMapper baseUserThirdMapper;

    private final OneKeyVerifier oneKeyVerifier;

    private final TransactionOps transactionOps;

    private final AsyncListenableTaskExecutor asyncExecutor;

    /**
     * 一键登陆
     *
     * @param token
     * @return
     */
    @PostMapping("/oneKey")
    public ResultVO<TokenUser> oneKey(@RequestParam String token, HttpServletRequest request) {

        String account = oneKeyVerifier.obtainPhone(token);

        long currentTime = currentTimeMillis();

        BaseUser principal = obtainUser(account, currentTime, null);

        return loginCommon(request, currentTime, principal);

    }

    /**
     * 账号密码登陆
     *
     * @param login
     * @param request
     * @return
     */
    @PostMapping("/password")
    public ResultVO<TokenUser> password(@Valid PasswordLogin login, HttpServletRequest request) {

        BaseUser principal = baseUserMapper.selectByAccount(login.account);

        if (principal == null) throw NONENTITY_USER_EXC;

        if (!passwordEncoder.matches(login.password, principal.password))
            throw CustomException.BAD_CREDENTIAL_EXC;

        return loginCommon(request, currentTimeMillis(), principal);

    }

    /**
     * 短信验证码登陆
     *
     * @param login
     * @param request
     * @return
     */
    @PostMapping("/smsCode")
    public ResultVO<TokenUser> smsCode(@Valid SmsLogin login, HttpServletRequest request) {

        smsService.verifyCode(login.account, SmsScene.login.name(), login.smsCode);

        long currentTime = currentTimeMillis();

        BaseUser principal = obtainUser(login.account, currentTime, null);

        return loginCommon(request, currentTime, principal);

    }

    private static final CustomException UNBOUND_THIRD_EXC = new CustomException(NONENTITY_USER, "Unbound third platforms");

    private static final CustomException ILLEGAL_THIRD_EXC = new CustomException(BAD_CREDENTIAL, "Illegal third token");

    /**
     * 第三方社交绑定
     *
     * @return
     */
    @PostMapping("/thirdBind")
    public ResultVO<TokenUser> thirdBind(@Valid ThirdBind bind, HttpServletRequest request) {

        ThirdVerifier thirdVerifier = thirdVerifierHolder.obtainThirdVerify(bind.provider);

        if (thirdVerifier.verify(bind.openId, bind.token)) throw ILLEGAL_THIRD_EXC;

        long currentTime = currentTimeMillis();

        String account = bind.account;

        if (StringUtils.isNotBlank(account)) {
            smsService.verifyCode(account, SmsScene.login.name(), bind.smsCode);
        } else if (StringUtils.isNotBlank(bind.oneKeyToken)) {
            account = oneKeyVerifier.obtainPhone(bind.oneKeyToken);
        } else throw BAD_PARAM_EXC;

        BaseUser principal = obtainUser(account, currentTime, bind);

        transactionOps.execute(s ->
                baseUserThirdMapper.insert(new BaseUserThird(bind.provider, bind.openId, principal.id, currentTime)));

        return loginCommon(request, currentTime, principal);

    }

    /**
     * 第三方社交登陆
     *
     * @param login
     * @param request
     * @return
     */
    @PostMapping("/third")
    public ResultVO<TokenUser> third(@Valid ThirdLogin login, HttpServletRequest request) {

        ThirdVerifier thirdVerifier = thirdVerifierHolder.obtainThirdVerify(login.provider);

        BaseUser principal = baseUserThirdMapper.selectByProviderAndOpenId(login.provider, login.openId);

        if (principal == null) throw UNBOUND_THIRD_EXC;

        if (thirdVerifier.verify(login.openId, login.token)) throw ILLEGAL_THIRD_EXC;

        return loginCommon(request, currentTimeMillis(), principal);

    }

    private ResultVO<TokenUser> loginCommon(HttpServletRequest request, long currentTime, BaseUser principal) {

        ClientInfo clientInfo = clientHolder.obtainClientDetails(request);

        AccessToken accessToken = createAccessToken(principal, clientInfo, currentTime, true);

        if (currentTime != principal.loginTime)
            asyncExecutor.execute(() -> baseUserMapper.updateLoginById(currentTime, principal.id));

        return ResultVO.success(TokenUser.of(accessToken, principal.id));
    }

    /**
     * token刷新
     *
     * @return
     */
    @GetMapping("/refreshToken")
    public ResultVO<AccessToken> refreshToken() {

        AuthContext context = AuthContextHolder.getAuthContext();

        long currentTime = currentTimeMillis();

        tokenService.verifyTokenRefresh(context.client, context.userId, context.expire, currentTime);

        BaseUser principal = baseUserMapper.selectById(context.userId);

        if (principal == null) throw NONENTITY_USER_EXC;

        ClientInfo clientInfo = clientHolder.obtainClientDetails(context.client);

        AccessToken accessToken = createAccessToken(principal, clientInfo, currentTime, false);

        asyncExecutor.execute(() -> baseUserMapper.updateActiveTimeById(currentTime, context.userId));

        return ResultVO.success(accessToken);

    }

    /**
     * 登陆通用
     *
     * @param principal
     * @param clientInfo
     * @param currentTime
     * @param loginOrRefresh
     * @return
     */
    private AccessToken createAccessToken(BaseUser principal, ClientInfo clientInfo,
                                          long currentTime, boolean loginOrRefresh) {

        long expire = currentTime + clientInfo.validity;

        int[] authorities = obtainAuthorities(principal.authorities, currentTime);

        TokenPayload payload = TokenPayload.newBuilder()
                .setClient(clientInfo.id)
                .setExpire(expire)
                .setId(principal.id)
                .addAllAuthorities(authorities)
                .build();

        return tokenService.createAccessToken(payload, loginOrRefresh);

    }

    /**
     * 用户通用注册并获取用户基本信息
     *
     * @param account
     * @param currentTime
     * @return
     */
    private BaseUser obtainUser(String account, long currentTime, ThirdBind bind) {
        BaseUser baseUser = baseUserMapper.selectByAccount(account);
        //用户不存在 新建一个用户并添加
        if (baseUser == null) {
            String nickname = bind == null ?
                    "新用户" + RandomStringUtils.random(6, "0123456789") : bind.nickname;
            BaseUser newBaseUser = new BaseUser()
                    .setNickname(nickname)
                    .setCreateTime(currentTime)
                    .setLoginTime(currentTime)
                    .setActiveTime(currentTime);
            transactionOps.execute(s -> baseUserMapper.insert(newBaseUser));
            baseUser = newBaseUser;
        }
        return baseUser;
    }


    /**
     * 删除refreshToken
     *
     * @return
     */
    @GetMapping("/logout")
    public ResultVO<Void> logout() {

        AuthContext authContext = AuthContextHolder.getAuthContext();

        if (authContext.isAuthenticated())
            tokenService.clearTokenCache(authContext.client, authContext.userId);

        return ResultVO.success();
    }

    private static final int[] EMPTY_INT_ARRAY = new int[0];

    private static final TypeReference<Map<String, Long>> TYPE_REF = new TypeReference<>() {};

    private int[] obtainAuthorities(String authoritiesJson, long currentTime) {
        Map<String, Long> authorities = jsonMapper.readValue(authoritiesJson, TYPE_REF);
        if (authorities != null) {
            Iterator<Entry<String, Long>> iterator = authorities.entrySet().iterator();
            while (iterator.hasNext()) {
                Entry<String, Long> entry = iterator.next();
                long expiration = entry.getValue();
                if (expiration > 0 && expiration < currentTime) iterator.remove();
            }
        }
        return authorities == null ? EMPTY_INT_ARRAY : authorities.keySet()
                .stream().mapToInt(Authority::obtainCode).toArray();
    }

}
